﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;

namespace Liphofra.Mvvm.Validation
{
    /// <summary>
    /// An implementation of the <see cref="IValidationManager"/> result that works with <see cref="IValidationRule"/> instances.
    /// To this end, it allows registration of such rules based on types and their properties.
    /// </summary>
    public class ValidationManager : IValidationManager
    {
        private readonly IDictionary<string, IList<IValidationRule>> _ruleMappings = new Dictionary<string, IList<IValidationRule>>();

        /// <summary>
        /// Registers a validation rule for the given type.
        /// </summary>
        /// <typeparam name="T">The type to register the validation rule for.</typeparam>
        /// <param name="rule">The rule to register for the type.</param>
        [SuppressMessage("Microsoft.Design", 
            "CA1004:GenericMethodsShouldProvideTypeParameter", 
            Justification = "We need to perform a type mapping here, no instance argument will be available.")]
        public void RegisterValidationRule<T>(IValidationRule rule)
        {
            if (rule == null)
            {
                throw new ArgumentNullException("rule");
            }

            var type = typeof(T);
            var key = type.FullName;

            RegisterRule(key, rule);
        }

        /// <summary>
        /// Registers the validation rule for the given type and property.
        /// </summary>
        /// <typeparam name="T">The type to register the validation rule for.</typeparam>
        /// <typeparam name="TProperty">The type of the property to register the validation rule for.</typeparam>
        /// <param name="rule">The rule to register for the type and property.</param>
        /// <param name="propertySelector">A lambda expression to select the property to register the rule for.</param>
        [SuppressMessage("Microsoft.Design",
            "CA1006:DoNotNestGenericTypesInMemberSignatures",
            Justification = "There is no other way of defining a property selector.")]
        [SuppressMessage("Microsoft.Design",
            "CA1011:ConsiderPassingBaseTypesAsParameters",
            Justification = "As clarity to the caller of this method, the full signature is used.")]
        public void RegisterValidationRule<T, TProperty>(IValidationRule rule, Expression<Func<T, TProperty>> propertySelector)
        {
            if (rule == null)
            {
                throw new ArgumentNullException("rule");
            }

            if (propertySelector == null)
            {
                throw new ArgumentNullException("propertySelector");
            }

            var memberExpression = propertySelector.Body as MemberExpression;
            if (memberExpression == null)
            {
                throw new ArgumentException("A member expression is required to define a property.", "propertySelector");
            }

            var propertyName = memberExpression.Member.Name;

            // create key
            var type = typeof(T);
            var key = CreateTypePropertyNameKey(type, propertyName);

            RegisterRule(key, rule);
        }

        private void RegisterRule(string key, IValidationRule rule)
        {
            IList<IValidationRule> rules;
            if (!_ruleMappings.TryGetValue(key, out rules))
            {
                rules = new List<IValidationRule>();
                _ruleMappings.Add(key, rules);
            }

            rules.Add(rule);
        }

        #region IValidationManager

        /// <summary>
        /// Validates the specified target.
        /// </summary>
        /// <typeparam name="T">The type of the target.</typeparam>
        /// <param name="target">The target to validate.</param>
        /// <returns>A list of validation results indicating the result of the individual validation rules used for validation.</returns>
        public ValidationResults Validate<T>(T target)
        {
            var rules = GetRulesFor<T>();

            // no rules, no validation
            if (rules == null)
            {
                return ValidationResults.Success;
            }

            // run all rules, aggregate results (but only non-successful)
            var results = rules
                .Select(rule => rule.Validate(target))
                .Where(result => result.HasErrors)
                .ToList();

            // do we have any results that are not successful?
            if (results.Count > 0)
            {
                return new ValidationResults(results);
            }

            // everything went perfectly
            return ValidationResults.Success;
        }

        /// <summary>
        /// Validates the specified property of the target.
        /// </summary>
        /// <typeparam name="T">The type of the target.</typeparam>
        /// <param name="target">The target to validate.</param>
        /// <param name="propertyName">The name of the property to validate on the target.</param>
        /// <returns>A list of validation results indicating the result of the individual validation rules used for validation.</returns>
        /// <exception cref="System.ArgumentNullException">Thrown when propertyName is null.</exception>
        public ValidationResults ValidateProperty<T>(T target, string propertyName)
        {
            if (string.IsNullOrWhiteSpace(propertyName))
            {
                throw new ArgumentNullException("propertyName");
            }

            var rules = GetRulesFor<T>(propertyName);

            // no rules, no validation
            if (rules == null || rules.Count == 0)
            {
                return ValidationResults.Success;
            }

            // run all rules, aggregate results (but only non-successful)
            var results = rules
                .Select(rule => rule.ValidateProperty(target, propertyName))
                .Where(result => result.HasErrors)
                .ToList();

            // do we have any results that are not successful?
            if (results.Count > 0)
            {
                return new ValidationResults(results);
            }

            // everything went perfectly
            return ValidationResults.Success;
        }

        /// <summary>
        /// Validates all properties for specified target type.
        /// </summary>
        /// <typeparam name="T">The type of the target.</typeparam>
        /// <param name="target">The target to validate.</param>
        /// <returns>
        /// A list of validation results indicating the result of the individual validation rules used for validation.
        /// </returns>
        public ValidationResults ValidateAllProperties<T>(T target)
        {
            var propertyNames = GetPropertyNamesWithRulesFor<T>();

            // run all rules and aggregate results
            var result = new List<ValidationResult>();
            foreach (var propertyName in propertyNames)
            {
                var validationResults = ValidateProperty(target, propertyName);

                if (validationResults.HasErrors)
                {
                    // add all results that are non-successful validations
                    result.AddRange(validationResults.Errors);
                }
            }

            // do we have any results that are not successful?
            if (result.Count > 0)
            {
                return new ValidationResults(result);
            }

            return ValidationResults.Success;
        }

        #endregion

        private static string CreateTypePropertyNameKey(Type type, string propertyName)
        {
            return string.Format(CultureInfo.InvariantCulture, "{0}.{1}", type.FullName, propertyName);
        }

        private IEnumerable<IValidationRule> GetRulesFor<T>()
        {
            var type = typeof(T);
            var key = type.FullName;
            IList<IValidationRule> rules;
            _ruleMappings.TryGetValue(key, out rules);
            return rules;
        }

        private IList<IValidationRule> GetRulesFor<T>(string propertyName)
        {
            var type = typeof(T);
            var key = CreateTypePropertyNameKey(type, propertyName);
            IList<IValidationRule> rules;
            _ruleMappings.TryGetValue(key, out rules);
            return rules;
        }

        private IEnumerable<string> GetPropertyNamesWithRulesFor<T>()
        {
            var type = typeof(T);
            var subKey = type.FullName;
            var propertyNames = new List<string>();
            foreach (var ruleMapping in _ruleMappings)
            {
                if (ruleMapping.Key.StartsWith(subKey, StringComparison.Ordinal) && ruleMapping.Key.Length > subKey.Length)
                {
                    var propertyName = ruleMapping.Key.Substring(subKey.Length + 1);
                    propertyNames.Add(propertyName);
                }
            }

            return propertyNames;
        }
    }
}